/* 
 * Copyright (C) 2014 Krzysztof Troska <elleander86 at gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package anidbjclient;

import java.io.File;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Imports and handles data from Anidb.
 *
 * @author Krzysztof Troska <elleander86 at gmail.com>
 */
public class AnidbHandler {

    private static final ConfigManager cm = new ConfigManager();

    private static String sessionID;
    public static boolean isLoged = false;
    public static boolean offlineMode = false;
    public static long offlineTime = 0;
    /**
     * Send Time to protect us from flood ban set -4000 so we can send first
     * packet right away
     */
    private static long sendTime = System.currentTimeMillis() - 4000;

    /**
     * Ready msg - adds \n at the end and if needed logs and/or adds session id
     * @param msg msg we want to send.
     * @return 
     * @throws AnidbException if we are not logged or can't log (wrong password)
     * anidb problems
     */
    private String readyMessage(String msg) throws AnidbException {
        /* commands for which we don't need to log */
        List<String> noLoggin = Arrays.asList("PING", "ENCRYPT", "ENCODING", 
                "AUTH", "VERSION");
        String command = msg.substring(0, msg.indexOf(" "));
        if (noLoggin.contains(command))
            if(msg.contains("\n"))
                return msg; // we already got \n at end so dont bother
            else
                return msg + "\n"; // command don't need login so we just add eol
        if (!isLoged) // log if not logged.
            connect(); // this can throw Anidb Exception
        if (msg.contains("&s=")) // drop session id if we got one 
            msg = msg.substring(0, msg.indexOf("&s="));
        msg = msg.concat("&s=" + sessionID + "\n");
        return msg;
    }

    /**
     * Process msg and sends it to anidb also checks response and returns it,
     * this function in synchronized - since we will get banned if we use too
     * many ports.
     * @param msg raw mesage to be send to anidb.
     * @param acceptCodes all codes we expect, all other codes will throw exception
     * if no code given we pass all responses as good
     * @return response from anidb or string "Error"
     * @throws 
     * AnidbOfflineException() when we are in offline mode. 
     * AnidbException() when we get a fatal error (like no login info given)
     */
    private synchronized String sendData(String msg, int... acceptCodes) throws 
            AnidbException {
        if(offlineMode) // if we are in offline mode we don't want to send any data
            throw new AnidbOfflineException();
        DatagramPacket packet; // packet we will send
        // There is *generealy* no way for our msg be longer than 1400 bytes
        // TODO: but we still want to tak care of that some time.
        byte[] buf;
        String response; // string ready for response
        long wait; // how long we will or not wait
        int tries = 0;
        while (true) {
            msg = readyMessage(msg);
            if(tries++ >= 3)
                throw new AnidbOfflineException("Can't get response after 3 tries.");
            try (DatagramSocket socket = new DatagramSocket(cm.getLocalPort())) {
                /* make msg anidb friendly */
               
                wait = System.currentTimeMillis() - sendTime;
                /**
                 * Wait at least 4 s from last send
                 */
                if (wait < 4000) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(wait);
                    } catch (InterruptedException ex) {
                        //for now we don't interupt that
                        System.err.println("Wait was interupted."); 
                    }
                }
                buf = msg.getBytes(); // send msg to buffer
                System.out.println("Sending msg: " + msg);
                packet = new DatagramPacket(buf, buf.length, cm.getServer(), cm.getPort());
                socket.send(packet);
                sendTime = System.currentTimeMillis();
                buf = new byte[cm.getOptionInt("BufSize")]; // clear buffer
                packet = new DatagramPacket(buf, buf.length);
                socket.setSoTimeout(10000); // 10 seconds wait before trying again
                socket.receive(packet);
                response = new String(buf).trim();
                int code = this.getResponseCode(response, msg);
                System.out.println("Received Packet: " + response);
                /* no accepted codes so we take all responses as good */
                if(acceptCodes.length == 0)
                    return response;
                /* if at least 1 accepted code is ok then we return response */
                for(int acceptCode : acceptCodes) {
                    if(acceptCode == code)
                        return response;
                }
                /* response did not pass code check so its a problem. */
                checkResponseCode(response, msg);
            } catch (AnidbNotLoggedException ex) {
                /**
                 * In case we are are not logged exception already set isloged 
                 * to false so now we just start again and let readyMessage()
                 * log us again.
                 */
                System.out.println("Session time out try atain.");
            } catch (SocketTimeoutException e) {
                /**
                 * Socket did time out so we start whole thing again.
                 */
                System.out.println("Socket Time out, trying again.");
            } catch (SocketException e) {
                throw new AnidbException("Can't create socket");
            } catch (IOException ex) {
                throw new AnidbException("Can't read from socket");
            }
        }
    }

    /**
     * Sends AUTH command to anidb.
     *
     * @throws AnidbException
     */
    private void connect() throws AnidbException {
        StringBuilder sb = new StringBuilder();
        sb.append("AUTH ");
        sb.append("user=");
        sb.append(cm.getUserName());
        sb.append("&pass=");
        sb.append(cm.getPassword());
        sb.append("&protover=");
        sb.append(cm.getOption("AnidbApiVersion"));
        sb.append("&client=anidbjclient");
        sb.append("&clientver=");
        sb.append(cm.getOption("Version"));
        String response = sendData(sb.toString(), 200, 201);
        int code = this.getResponseCode(response);
        String[] responseArray = response.split(" ");
        if (code == 200) {
            sessionID = responseArray[1];
            System.out.println("Session set: " + sessionID);
            isLoged = true;
            return;
        } else if (code == 201) {
            sessionID = responseArray[1];
            System.out.println("Session set: " + sessionID);
            isLoged = true;
            System.err.println("New Version avaible, consider upgrading.");
            return;
        }
        throw new AnidbException(response);
    }
    
    /**
     * Gets response code from anidb response, if there is problem with that 
     * it throws AnidbException with command and response so we know what went 
     * wrong.
     * @param response - response from anidb
     * @param command - command we send to anidb
     * @return - int containing XXX anidb response code.
     * @throws AnidbException in case response code can't be read
     */
    private int getResponseCode(String response, String command) throws AnidbException {
        int code;
        try {
            code = Integer.parseInt(response.substring(0, 3));
        } catch (NumberFormatException e) {
            // should not happend but still.
            throw new AnidbException(response, command);
        }
        return code;
    }
    
    /**
     * Gets response code from anidb response, if there is problem with that 
     * it throws AnidbException with response we got so we know what went wrong.
     * @param response - response from anidb
     * @return - int containing XXX anidb response code.
     * @throws AnidbException in case response code can't be read
     */
    private int getResponseCode(String response) throws AnidbException {
        int code;
        try {
            code = Integer.parseInt(response.substring(0, 3));
        } catch (NumberFormatException e) {
            // should not happend but still.
            throw new AnidbException(response);
        }
        return code;
    }

    /**
     * Checks response code and handles that with throwing good anidb exception
     * @param response full response from anidb server
     * @param command full command we sended for anidb server for reference
     * @throws AnidbException Can and will throw any kind of anidb exception
     * depending on error code.
     */
    private void checkResponseCode(String response, String command) throws AnidbException {
        int code = this.getResponseCode(response, command);
        // get all after error code and drop new line mark
        String msg = response.substring(4).replaceAll("\\n", "");
        if (code >= 700) {
            throw new AnidbOfflineException("Unknow error code", msg);
        } else if (code >= 600) { //internal anidb error
            //TODO: should make some wait option for next questions 
            throw new AnidbOfflineException("Internal Anidb error", msg);
        }
        switch (code) {
            case 500:
                throw new AnidbOfflineException("Wrong login and password"
                        + " in config file");
            case 501:
            case 502:
            case 506:
                throw new AnidbNotLoggedException();
            // Banned:
            case 555:
                throw new AnidbException(msg);
            //other prodobly our foult
            case 598:
            case 505:
            default:
                throw new AnidbException(msg, command);
        }
    }

    /**
     * We just want to import data from AniDB to local DB,
     *
     * @param path to the file we want to import information
     * @param hash String containing ed2k hash
     * @return String array for later add into local db.
     * @throws AnidbException in case we did not import data from anidb
     */
    protected String[] importFileInfo(Path path, String hash) throws AnidbException {
        File file = path.toFile();
        StringBuilder sb = new StringBuilder();
        sb.append("FILE ");
        sb.append("size=");
        sb.append(file.length());
        sb.append("&ed2k=");
        sb.append(hash);
        sb.append("&fmask=");
        sb.append(cm.getOption("FMask"));
        sb.append("&amask=");
        sb.append(cm.getOption("AFileMask"));
        /**
         * Getting info from anidb and splitting it by lines 0 contains number 1
         * the data
         */
        String response = sendData(sb.toString(), 220, 320);
        int code = this.getResponseCode(response);
        if (code == 220) {
            String[] tmp = response.split("\n");
            String[] tmp2 = tmp[1].split("\\|");
            /**
             * Thx to fmask: tmp2: 0-fid 1-aid 2-eid 3-gid 4-myListId 5-air date
             * 6 mylist state 7-mylistfilestate 8- viewdate Thx to a_file_mask:
             * 9- group name Other: 10- hash
             */
            return addStringArrays(tmp2, hash);
        } else if (code == 320) {
            throw new AnidbMissingContentException(response);
        }
        throw new AnidbException(response);
    }

    /**
     * Imports episode info
     *
     * @param eid
     * @return array of strings (can have some empty values)
     * @throws AnidbException
     */
    protected String[] importEpisodeInfo(int eid) throws AnidbException {
        StringBuilder sb = new StringBuilder();
        sb.append("EPISODE ");
        sb.append("eid=");
        sb.append(eid);
        String response = sendData(sb.toString(), 240, 340);
        int code = this.getResponseCode(response);
        if (code == 240) {
            String[] tmp = response.split("\n");
            String[] tmp2 = tmp[1].split("\\|");
            /**
             * 0- eid 1-aid 2-lenght 3- rating 4- votes 5-ep_number 6-eng_name
             * 7- romaji_name 8-kanji 9-aired 10-type
             */
            return tmp2;
        } else if (code == 340) {
            throw new AnidbMissingContentException(response);
        }
        throw new AnidbException(response);
    }

    /**
     * Imports group info
     *
     * @param gid
     * @return array of strings (can have some empty values)
     * @throws AnidbException
     */
    protected String[] importGroupInfo(int gid) throws AnidbException {
        StringBuilder sb = new StringBuilder();
        sb.append("GROUP ");
        sb.append("gid=");
        sb.append(gid);
        String response = sendData(sb.toString(), 250, 350);
        int code = this.getResponseCode(response);
        if (code == 250) {
            String[] tmp = response.split("\n");
            String[] tmp2 = tmp[1].split("\\|");
            /**
             * 0-gid 1-rating 2-votes 3-acount 4-fcount 5-name 6-short
             * 7-ircChannel 8-ircServer 9-url 10-picname 11-founddate
             * 12-disbandeddate 13-dateflags 14-lastrelesedate
             * 15-lastactivitydate 16-groupdrelations
             */
            return tmp2;
        } else if (code == 350) {
            throw new AnidbMissingContentException(response);
        }
        throw new AnidbException(response);
    }

    /**
     * Imports anime description from anidb.
     * @param aid - 
     * @return
     * @throws AnidbException 
     */
    protected String importAnimeDescription(int aid) throws AnidbException {
        int part = 0, parts, code;
        String query, response;
        String[] tmp, tmp2;
        StringBuilder description = new StringBuilder();
        do {
            query = animeDescriptionQuery(aid, part);
            response = sendData(query, 233, 333);
            code = this.getResponseCode(response);            
            tmp = response.split("\n");
            if (code == 233) {
                tmp2 = tmp[1].split("\\|");
                parts = Integer.parseInt(tmp2[1]);
                // this if is Anidb BUG:0002072 walk around
                if (tmp2.length == 3 && !tmp2[2].isEmpty()) {
                    description.append(tmp2[2]);
                }
            } else if (code == 333) {
                throw new AnidbMissingContentException(response);
            } else {
                throw new AnidbException(response);
            }
            part++;
        } while (part <= parts);
        return description.toString().replaceAll("<cut>", "");
    }

    ;
    
    /**
     * Returns query to ready to be send for next part of anime desc
     * @param aid int anime id 
     * @param part int part of desc
     * @return query to be send
     */
    private String animeDescriptionQuery(int aid, int part) {
        StringBuilder sb = new StringBuilder();
        sb.append("ANIMEDESC ");
        sb.append("aid=");
        sb.append(aid);
        sb.append("&part=");
        sb.append(part);
        return sb.toString();
    }

    /**
     * Returns my list info for given my list id
     *
     * @param myListId - my list id
     * @return response
     * @throws AnidbException
     */
    protected String[] importMyListInfo(int myListId) throws AnidbException {
        StringBuilder sb = new StringBuilder();
        sb.append("MYLIST ");
        sb.append("lid=");
        sb.append(myListId);
        String response = sendData(sb.toString(), 221, 321);
        int code = this.getResponseCode(response);
        if (code == 221) {
            String[] tmp = response.split("\n");
            String[] tmp2 = tmp[1].split("\\|");
            /**
             * 0 - lid, 1 - fid, 2 - eid, 3- aid, 4-gid, 5-date, 6- state,
             * 7-viewdate, 8-storage, 9-source, 10-other, 11-filestate
             */
            return tmp2;
        } else if (code == 321) {
            throw new AnidbMissingContentException(response);
        }
        throw new AnidbException(response);
    }

    /**
     * Gets additional info from anidb accessible only by ANIME command.
     *
     * @param aid anime id to get additional info
     * @return String array: 0 aid/ 1 type/ 2 18+ rest./
     * @throws AnidbException
     */
    protected String[] importAnimeInfo(int aid) throws AnidbException {
        StringBuilder sb = new StringBuilder();
        sb.append("ANIME ");
        sb.append("aid=");
        sb.append(aid);
        sb.append("&amask=");
        sb.append(cm.getOption("AAnimeMask"));
        String response = sendData(sb.toString(), 230, 330);
        int code = this.getResponseCode(response);
        if (code == 230) {
            String[] tmp = response.split("\n");
            String[] tmp2 = tmp[1].split("\\|");
            /**
             * tmp2: 0 aid/ 1 dateflags/ 2 year /3 type/ 4 name/5 episodes/6 air
             * date/ 7end date/ 8 pic name/ 9 18+ rest./
             */
            return tmp2;
        } else if (code == 330) {
            throw new AnidbMissingContentException(response);
        }
        throw new AnidbException(response);
    }

    private String[] addStringArrays(String[] a, String b) {
        String[] result = new String[a.length + 1];
        System.arraycopy(a, 0, result, 0, a.length);
        result[a.length] = b;
        return result;
    }

    /**
     * Adds file to my list
     *
     * @param fid file id
     * @param viewDate time of view in unix time stamp
     * @return int my list id
     * @throws AnidbException
     */
    protected int addToMyList(int fid, long viewDate) throws AnidbException {
        StringBuilder sb = new StringBuilder();
        sb.append("MYLISTADD ");
        sb.append("fid=");
        sb.append(fid);
        sb.append("&state=");
        sb.append("1");
        sb.append("&viewed=");
        if (viewDate > 0) {
            sb.append("1");
            sb.append("&viewdate=");
            sb.append(viewDate);
        } else {
            sb.append("0");
        }
        String response = sendData(sb.toString(), 210, 310);
        int code = this.getResponseCode(response);
        String[] tmp = response.split("\n");
        if (code == 210) {
            return Integer.parseInt(tmp[1]);
        } else if (code == 310) {
            String[] tmp2 = tmp[1].split("\\|");
            return Integer.parseInt(tmp2[0]);
        }
        throw new AnidbException(response);
    }

    /**
     * Updates my lsit entry
     *
     * @param lid myListId
     * @param state int state of file see
     * http://wiki.anidb.net/w/UDP_API_Definition#MYLIST:_Retrieve_MyList_Data
     * @param viewDate unix time of watch 0 if not watched
     * @throws AnidbException
     */
    protected void editMyList(int lid, int state, long viewDate) throws AnidbException {
        StringBuilder sb = new StringBuilder();
        sb.append("MYLISTADD ");
        sb.append("lid=");
        sb.append(lid);
        sb.append("&state=");
        sb.append(state);
        sb.append("&viewed=");
        if (viewDate > 0) {
            sb.append("1");
            sb.append("&viewdate=");
            sb.append(viewDate);
        } else {
            sb.append("0");
        }
        sb.append("&edit=1");
        String response = sendData(sb.toString(), 210, 311);
        int code = this.getResponseCode(response);
        if (code == 210 || code == 311)
            return;
        throw new AnidbException(response);
    }
}
